查看原文
其他

The House of Mind

jmpcall 看雪学苑 2022-07-01


本文为看雪论坛精华‍‍‍文章
看雪论坛作者ID:jmpcall




"The House of Mind"的学习条件


其实不光是"The House of Mind",在学习各种堆溢出漏洞的利用方法之前,都必须对glibc malloc()/free()的逻辑,有相当程度的了解,《Glibc内存管理--Ptmalloc2源代码分析》这份文档,通过129页的篇幅,已经分析的非常深刻和详细(如果没有积分下载文档,也可以去看作者的博客:https://www.iteye.com/blog/user/mqzhuang),也可以看看我发过的一个帖子:https://bbs.pediy.com/thread-271331.htm

先从外围了解glibc malloc()/free()的本质和设计目标,瞄一眼宏观的地形,再深入到茫茫的内部实现中,应该可以少迷点路。

另外,本文是对phrack杂志中一篇神作的总结和补充,所以exploit code和更完整的分析过程,请阅读原文:
http://phrack.org/issues/66/10.html。





什么是"The House of Mind"?


"The House of Mind"是一种堆溢出漏洞的利用方法(为什么叫这个名称我目前还不知道),可以通过构造输入数据,让漏洞程序执行攻击者期望的任意代码(不过,不是所有存在堆溢出漏洞的程序,都可以利用这种方法进行攻击,需要漏洞程序满足一定条件,稍后具体说明)。

再具体一点就是,在应用程序分配到的内存周围,很多都是glibc内部使用的内存,程序存在漏洞,攻击者就有机会通过构造输入数据,溢出glibc内部使用的变量,进一步控制malloc()/free()的执行逻辑,最终借glibc之手,修改某个函数对应的got表项(可以理解为函数指针,感兴趣也可以看看我的另外一个帖子:
https://bbs.pediy.com/thread-246373.htm),使其指向一段shell code(同样通过用户输入构造)。

这样,当漏洞程序后续执行该函数时(比如.dtors()函数,它会在main()函数结束后执行),就会触发shell code执行。

"The House of Mind"的目标,就是欺骗_int_free()按如下逻辑执行:




漏洞程序


为了满足"The House of Mind"的利用条件,作者提供了一个用于演示的漏洞程序(现实中这类漏洞当然会隐蔽的多,几乎不会存在这么饥渴难耐的想被宰割的程序)。
/* * K-sPecial's vulnerable program */ #include <stdio.h>#include <stdlib.h> int main (void) { char *ptr = malloc(1024); /* First allocated chunk */ char *ptr2; /* Second chunk */ /* ptr & ~(HEAP_MAX_SIZE-1) = 0x08000000 */ int heap = (int)ptr & 0xFFF00000; _Bool found = 0; printf("ptr found at %p\n", ptr); /* Print address of first chunk */ // i == 2 because this is my second chunk to allocate for (int i = 2; i < 1024; i++) { /* Allocate chunks up to 0x08100000 */ if (!found && (((int)(ptr2 = malloc(1024)) & 0xFFF00000) == \ (heap + 0x100000))) { printf("good heap allignment found on malloc() %i (%p)\n", i, ptr2); found = 1; /* Go out */ break; } } malloc(1024); /* Request another chunk: (ptr2 != av->top) */ /* Incorrect input: 1048576 bytes */ fread (ptr, 1024 * 1024, 1, stdin); free(ptr); /* Free first chunk */ free(ptr2); /* The House of Mind */ return(0); /* Bye */}

按照程序逻辑,归纳程序的执行流程如下:


① ptr = malloc(1024);

heap = (int)ptr & 0xFFF00000; // 将ptr值按1M向下取整


② 循环执行ptr2 = malloc(1024),直到(ptr2 & 0xFFF00000) == (heap + 0x100000)


③ 再次执行一次malloc(1024)


④ fread(ptr, 1024 * 1024, 1, stdin);

使用fead()函数读取用户输入,是为了减小攻击难度,如果换成strcpy()函数,读到'\0'字符就不会再读了,解决这个问题,要使用的就是另外的技术了,作者为了让大家专注于"The House of Mind",就特地避开了更复杂的情况。


⑤ free(ptr);

free(ptr2);


稍后就会明白为什么这样才能满足"The House of Mind"的利用条件,先看漏洞程序的内存布局(左):


由于glibc在每个chunk头部,都额外安排了4字节的size字段记录其大小,并将整个chunk的大小按8字节对齐,所以每次malloc(1024)实际消耗的是1032字节((1024+4)按8对齐),因此在循环中,ptr2是按1032字节递增的,这样,假设实际运行时ptr=0x804a008(如果/proc/sys/kernel/randomize_va_space文件内容非0,ptr值会是随机的。

即使随机化是关闭的,也跟漏洞程序代码段、数据段的长度有关,可以认为攻击者无法精确预测这个值,不过它也不会影响能否攻击成功,定个假设值只是为了后续描述方便),循环第723次时,ptr2=0x81002a0,跳出循环。

蓝色区域对应第一次malloc(1024)占用的内存,灰色区域对应循环中前721次malloc(1024)占用的内存,白色区域对应循环第722次malloc(1024)占用的内存,橙色区域对应循环第723次malloc(1024)占用的内存;

利用size字段,对任意chunk计算其结束位置,也就是next chunk的开始位置,是很容易的,如果只是为了划清各个chunk之间的界线,有size字段其实就够了,但是在释放过程中,如果当前释放chunk的prev chunk为free chunk,这时要是能知道prev chunk的大小,也是很有价值的。

因为这样就可以很方便计算prev chunk的起始位置,进而跟当前释放chunk合并,相反,如果prev chunk为inuse chunk,即不需要与当前释放chunk合并,那也就不需要知道prev chunk的大小了。

所以,glibc并不是始终为每个chunk安排一个prev_size字段,而是将free chunk的末尾4字节作为其next chunk的prev_size(一方面,既然是free chunk,那就表示业务层不会继续使用user data了,当然就可以被glibc内部使用;另一方面,prev_size相对于各个chunk头部的位置是确定的,向前偏移4字节就是);

每个chunk的size都是0x409(最低3位清0为0x408,表示chunk总大小为1032字节,最低3位二进制值为001,表示A=0、M=0、P=1),是可以根据向malloc()传的大小推测的。

在构造溢出数据时,尽量保持各个size的原值,另外,刚分配完时,所有chunk都是inuse状态,所以prev_size字段所占空间,都是用于user data,不管被溢出数据填充成什么内容,都不会影响glibc内部的执行逻辑。



攻击过程分析


fread(ptr, 1024 * 1024, 1, stdin)这行代码,使攻击者有机会往0x804a008之后的1M内存,写入任意数据,这块内存中,有很多地方保存的是chunk->size,由glibc内部使用,通过构造溢出数据,控制这些地方的值,就可以达到欺骗glibc的效果,甚至还可以进一步欺骗glibc。

将部分user data也当作自己内部使用的内存,为此,作者构造出了上述内存布局图(右)中的数据,当漏洞程序执行free(ptr2)时,glibc就会按照攻击者欺骗的流程执行。

(1) chunk2->size = 0x40d
最低3位清0为0x408,表示chunk总大小为1032字节,最低3位二进制值为101,表示A=1、M=0、P=1,这是要欺骗glibc认为chunk2在thread arena中(chunk2实际在main arena中)。

然后进一步欺骗glibc,使其认为chunk2所属arena的管理结构,在输入数据可以溢出到的某个位置,这样就相当于控制了chunk2所属arena的管理结构的内容了(而main_arena是个变局变量,位于数据段,溢出数据到达不了)。
#define heap_for_ptr(ptr) \((heap_info *)((unsigned long)(ptr) & ~(HEAP_MAX_SIZE-1)))#define arena_for_chunk(ptr) \(chunk_non_main_arena(ptr) ? heap_for_ptr(ptr)->ar_ptr : &main_arena)

通过这段代码可以看出,glibc是将chunk2的地址按1M(HEAP_MAX_SIZE)向下对齐(即0x8100000),将其当作chunk2所属heap的地址,然后再将heap->ar_ptr指向的内存,作为chunk2所属arena的管理信息(这也是漏洞程序中用for循环分配内存,直到ptr2达到一直高度的原因,否则向下对齐为0x8000000,就不在可以溢出的范围了)。

(2) fake_heap->ar_ptr = 0x804a014
由于ar_ptr为heap_info结构的第一个成员,所以按照布局图中的构造地址,glibc会认为arena管理结构位于0x804a014。

(3) fake_arena->bins[2] = DTORS_END-12
根据布局图可以看出,fake_arena开始的8个字节,都被构造为0x102,剩余部分全部构造为DTORS_END-12,根据malloc_state结构的定义可知,这样构造肯定可以使fake_arena->bins[2] = DTORS_END-12。
struct malloc_state {/* Serialize access. */mutex_t mutex;// Should we have padding to move the mutex to its own cache line?#if THREAD_STATS/* Statistics for locking. Only used if THREAD_STATS is defined. */long stat_lock_direct, stat_lock_loop, stat_lock_wait;#endif/* The maximum chunk size to be eligible for fastbin */INTERNAL_SIZE_T max_fast; /* low 2 bits used as flags *//* Fastbins */mfastbinptr fastbins[NFASTBINS];/* Base of the topmost chunk -- not otherwise kept in a bin */mchunkptr top;/* The remainder from the most recent split of a small request */mchunkptr last_remainder;/* Normal bins packed as described above */mchunkptr bins[NBINS * 2];/* Bitmap of bins */unsigned int binmap[BINMAPSIZE];/* Linked list */struct malloc_state *next;/* Memory allocated from the system in this arena. */INTERNAL_SIZE_T system_mem;INTERNAL_SIZE_T max_system_mem;};

使fake_arena->bins[2] = DTORS_END-12,是为了欺骗glibc修改got[.dtros]:
} elseclear_inuse_bit_at_offset(nextchunk, 0); /*Place the chunk in unsorted chunk list. Chunks arenot placed into regular bins until after they havebeen given one chance to be used in malloc. */ bck = unsorted_chunks(av); // 返回:&fake_arena->bins[0] fwd = bck->fd; // fd位于malloc_chunk结构体8字节偏移处 // 所以fwd = bck->fd = fake_arena->bins[2] = DTORS_END-12 p->bk = bck; p->fd = fwd; bck->fd = p; // fake_arena->bins[2] = p fwd->bk = p; // bk位于malloc_chunk结构体12字节偏移处 // 所以这里会将p,写到DTORS_END指向的内存单元,即:got[.dtors] = p set_head(p, size | PREV_INUSE); set_foot(p, size); check_free_chunk(av, p);}

关于unsorted_chunks()函数返回&fake_arena->bins[0]的设计意图,可以进入这篇帖子:https://bbs.pediy.com/thread-271331.htm,看看其中的bin结构图。

(4) ((struct malloc_chunk*)(DTORS_END-12))->bk = p
在(3)中已经一起解释了,是为了使got[.dtors] = p,代码中的p,对应的是布局图中的chunk2,所以等到.dtors()函数执行时,实际上是执行chunk2位置的"nop; nop; jmp 0x0c"(nop机器码为0x90,jmp 0x0c机器码为0xeb0c,所以共4字节)。

(5) jmp 0x0c
向前跳转0x0c偏移,是因为接紧着的4个字节,用于存放0x40d,再往后的8字节,会被上述代码中的p->bk=bck和p->fd=fwd两条赋值语句覆盖,所以shell code一定要构造在chunk2->bk之后的位置。

(6) fake_arena = 0x804a014
0x804a014也就是在(2)中,为fake_heap->ar_ptr构造的值,作者一开始是将fake_arena构造在0x804a010位置的,还特地在0x804a010位置构造了一个0,作为fake_arena->mutex值,但是他通过调试发现,漏洞程序在执行完free(ptr)后,会将0x804a014位置清0,这样,为fake_arena->max_fast构造的0x102(为了使判断2、判断5不成立),就被覆盖了,从而使后续的攻击逻辑执行失败。作者说被覆盖的原因,可能是由于_int_free()结束之后,执行mutex_unlock()导致的:

我认为不是这个原因,通过布局图可以看出,构造数据并没有修改chunk->size,所以free(ptr)就是按照正常的glibc逻辑执行的,那么mutex_unlock()修改的一定是main_arena全局全量中的mutex,而不可能是这里,一些其它版本的glibc中,0x804a010、0x804a014位置分别会是fd_nextsize、bk_nextsize。

如果释放的chunk不是large chunk,释放函数中就会将这两个值清0,但是碰巧的是,glibc-2.3.6的malloc_chunk结构,没fd_nextsize、bk_nextsize成员,所以如果感兴趣,可以仔细看看代码确定一下。

(7) fake_arena->max_fast = 0x102
在(6)中已经说过了,free(ptr)结束后,会将0x804a014位置清0,所以正好可以作为fake_arena->mutex值,0x804a018位置的0x102,作为fake_arena->max_fast值。

(8) 最后调用的malloc()
漏洞程序退出循环后,又调用了一次malloc(),这也是为了满足"The House of Mind"的利用条件(使判断10不成立)。




攻击可行性证明


  上述已经将攻击流程介绍完毕,但是实际能否成功,还要看到底能不能将glibc欺骗到"fwd->bk = p;"这一行代码上。

判断1
/** p > -size, 即:p > 0-size,即:p+size > 0,在p、size都大于0的前提下,表示p+size溢出了,但是由于p和size都是无符号数,p+size >= 0恒成立,所以写成p > -size* 个人感觉更严谨应该是:p >= -size,因为p+size == 0,也表示溢出了(比如size=0x00000001,则p=-size=0xffffffff(取反+1)时,p+size==0也为溢出)*/if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) || __builtin_expect ((uintptr_t) p & MALLOC_ALIGN_MASK, 0))

由于0x40d相比于0x409,只是设置了NON_MAIN_ARENA标志位,并没有将chunk2的大小修改为异常值,也没有修改ptr2的指向,所以这个判断不成立。

判断2
if ((unsigned long)(size) <= (unsigned long)(av->max_fast)#if TRIM_FASTBINS /* If TRIM_FASTBINS set, don't place chunks bordering top into fastbins */ && (chunk_at_offset(p, size) != av->top)#endif )

根据构造数据可知,chunk2->size = 0x40d(最低3位为PREV_INUSE、IS_MMAPPED、NON_MAIN_ARENA标志位),av即为图中的fake_arena,而fake_arena->max_fast = 0x102(最低2位为FASTCHUNKS_BIT、NONCONTIGUOUS_BIT标志位),所以这个判断不成立。

判断3
else if (!chunk_is_mmapped(p))

由于chunk2->size = 0x40d,IS_MMAPPED标志位为0,表示不是直接从mmap内存区域分配,而是从main arena或者thread arena中分配的,所以这个判断成立。

判断4
if (__builtin_expect (p == av->top, 0))

由于攻击者欺骗了glibc,使其认为chunk2属于自己构造的fake_arena,而不再是main_arena,并且通过布局图可知,chunk2 = 0x8100298,fake_arena->top = DTORS_END-12,所以,这个判断不成立。

判断5
if (__builtin_expect (contiguous (av) && (char *) nextchunk >= ((char *) av->top + chunksize(av->top)), 0))

攻击者显然不希望这个判断成立,由于这2个判断条件之间是&&的关系,所以只要满足contiguous(av) == 0,即fake_arena->max_fast的NONCONTIGUOUS_BIT标志位为0,而根据布局图可知,fake_arena->max_fast = 0x102,可以保证这个判断不成立。

判断6
if (__builtin_expect (!prev_inuse(nextchunk), 0))

prev_inuse(nextchunk)用于判断chunk2是否已经是free chunk了,如果是,那就是double free,不过由于这是第一次释放chunk2,所以,这个判断不成立。

判断7
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)|| __builtin_expect (nextsize >= av->system_mem, 0))

作者使用的shell code很短,不会将chunk2->nextchunk->size覆盖为异常值,即使shell code很长,也可以通过jmp跳过nextchunk->size字段,另外av->system_mem,在攻击者可以构造的范围,所以,可以保证这个判断不成立。

判断8
if (!prev_inuse(p))

chunk2->size = 0x40d,PREV_INUSE标志位为1,显然不会通过这个判断,将chunk2与其前一个chunk合并。

判断9
if (nextchunk != av->top)

和控制判断4的道理一样,由于av->top的值,是受攻击者控制的,所以相应也很容易控制这个判断,使其成立。

判断10
if (!nextinuse)

chunk2->nextchunk,也就是漏洞程序中最后一次执行malloc()分配的chunk,它这时显然还没有释放,所以这个判断不成立,从而最终欺骗glibc执行到else分支中的代码(如果漏洞程序中没有最后一次malloc(),应该也能攻击成功,因为那样的话,chunk2后面就是top chunk,而top chunk一定是inuse状态的,这是通过它顶部的fencepost标记的)。



glibc改造


随着各种攻击技术的出现,glibc其实一直都在改造,以上看到的这些判断,很多就是为了缓解攻击,但glibc的改造,是受限于两个因素的:


不能影响正常逻辑

不能添加一个判断后,正常的逻辑也不对了。


不能影响性能
比如业务层调用free(),glibc会将释放chunk放在相应的缓存链表中,而判断是否double free,只会拿当前释放chunk和链表头中的第一个chunk,进行地址对比,而不会遍历整个链表对比。
所以,glibc只能缓存攻击,根本避免被攻击,在业务层的源头就要开始防范。




看雪ID:jmpcall

https://bbs.pediy.com/user-home-815036.htm

*本文由看雪论坛 jmpcall 原创,转载请注明来自看雪社区



# 往期推荐

1.CVE-2012-1889 暴雷漏洞分析与利用小记

2.人工智能竞赛-目标识别指导

3.Android加壳脱壳学习—动态加载和类加载机制详解

4.某APP sig3 48位算法逆向分析

5.CVE-2021-26411漏洞分析笔记

6.Windows本地提权漏洞CVE-2014-1767分析及EXP编写指导






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存